🎖️GitЯра🎖️
Node / meshtastic / Meshtastic-Android / files / core / network / src / commonMain / kotlin / org / meshtastic / core / network / radio / BleReconnectPolicy.kt
Displaying Raw • Download
core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleReconnectPolicy.kt renovate/net.java.dev.jna-jna-5.x (5a8f4966) Text, 8.05 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.network.radio
Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787kotlinx.coroutines.delay
Tff7b72import T7ee787kotlinx.coroutines.isActive
Tff7b72import T7ee787kotlin.coroutines.coroutineContext
Tff7b72import T7ee787kotlin.time.Duration
Tff7b72import T7ee787kotlin.time.Duration.Companion.seconds
T8b949e/**
* Encapsulates the BLE reconnection policy with exponential backoff.
*
* The policy tracks consecutive failures and decides whether to retry or signal a transient disconnect (DeviceSleep).
* When [maxFailures] is reached the [execute] loop invokes [execute]'s `onPermanentDisconnect` callback and returns;
* set [maxFailures] to [Int.MAX_VALUE] (as [BleRadioTransport] does) to disable the give-up path entirely.
*
* @param maxFailures maximum consecutive failures before giving up; use [Int.MAX_VALUE] to retry indefinitely
* @param failureThreshold after this many consecutive failures, signal a transient disconnect
* @param settleDelay delay before each connection attempt to let the BLE stack settle
* @param minStableConnection minimum time a connection must stay up to be considered "stable"
* @param backoffStrategy computes the backoff delay for a given failure count
*/
Tff7b72class T56d364BleReconnectPolicyTb4b4b4(
Tff7b72private Tff7b72val Te6edf3maxFailuresTb4b4b4: Tffa657Int Tff7b72= Te6edf3DEFAULT_MAX_FAILURESTb4b4b4,
Tff7b72private Tff7b72val Te6edf3failureThresholdTb4b4b4: Tffa657Int Tff7b72= Te6edf3DEFAULT_FAILURE_THRESHOLDTb4b4b4,
Tff7b72private Tff7b72val Te6edf3settleDelayTb4b4b4: Te6edf3Duration Tff7b72= Te6edf3DEFAULT_SETTLE_DELAYTb4b4b4,
T8b949e/** Minimum time a connection must stay up to be considered "stable". Exposed for callers to compare uptime. */
Tff7b72val Te6edf3minStableConnectionTb4b4b4: Te6edf3Duration Tff7b72= Te6edf3DEFAULT_MIN_STABLE_CONNECTIONTb4b4b4,
Tff7b72private Tff7b72val Te6edf3backoffStrategyTb4b4b4: Tb4b4b4(Te6edf3attemptTb4b4b4: Tffa657IntTb4b4b4) Tff7b72-Tff7b72> Te6edf3Duration Tff7b72= Tff7b72::Te6edf3computeReconnectBackoffTb4b4b4,
Tb4b4b4) Tb4b4b4{
T8b949e/** Outcome of a single reconnect iteration. */
Tff7b72sealed Tff7b72interface T56d364Outcome Tb4b4b4{
T8b949e/** Connection attempt succeeded and then eventually disconnected. */
Tff7b72data Tff7b72class T56d364DisconnectedTb4b4b4(Tff7b72val Te6edf3wasStableTb4b4b4: Tffa657BooleanTb4b4b4, Tff7b72val Te6edf3wasIntentionalTb4b4b4: Tffa657BooleanTb4b4b4) Tb4b4b4: Te6edf3Outcome
T8b949e/** Connection attempt failed with an exception. */
Tff7b72data Tff7b72class T56d364FailedTb4b4b4(Tff7b72val Te6edf3errorTb4b4b4: Te6edf3ThrowableTb4b4b4) Tb4b4b4: Te6edf3Outcome
Tb4b4b4}
T8b949e/** Action the caller should take after the policy processes an outcome. */
Tff7b72sealed Tff7b72interface T56d364Action Tb4b4b4{
T8b949e/** Retry the connection after the specified backoff delay. */
Tff7b72data Tff7b72class T56d364RetryTb4b4b4(Tff7b72val Te6edf3backoffTb4b4b4: Te6edf3DurationTb4b4b4) Tb4b4b4: Te6edf3Action
T8b949e/** Signal a transient disconnect to higher layers. */
Tff7b72data Tff7b72class T56d364SignalTransientTb4b4b4(Tff7b72val Te6edf3backoffTb4b4b4: Te6edf3DurationTb4b4b4) Tb4b4b4: Te6edf3Action
T8b949e/** Give up permanently. */
Tff7b72data Tff7b72object T56d364GiveUp Tb4b4b4: Te6edf3Action
T8b949e/** Continue immediately (e.g. after an intentional disconnect). */
Tff7b72data Tff7b72object T56d364Continue Tb4b4b4: Te6edf3Action
Tb4b4b4}
Tff7b72internal Tff7b72var Te6edf3consecutiveFailuresTb4b4b4: Tffa657Int Tff7b72= T79c0ff0
Te6edf3private Tff7b72set
T8b949e/** Processes the outcome of a connection attempt and returns the action the caller should take. */
Tff7b72fun Td2a8ffprocessOutcomeTb4b4b4(Te6edf3outcomeTb4b4b4: Te6edf3OutcomeTb4b4b4)Tb4b4b4: Te6edf3Action Tff7b72= Tff7b72when Tb4b4b4(Te6edf3outcomeTb4b4b4) Tb4b4b4{
Tff7b72is Te6edf3OutcomeTb4b4b4.Te6edf3Disconnected Tff7b72-Tff7b72> Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3outcomeTb4b4b4.Te6edf3wasIntentionalTb4b4b4) Tb4b4b4{
Te6edf3consecutiveFailures Tff7b72= T79c0ff0
Te6edf3ActionTb4b4b4.Te6edf3Continue
Tb4b4b4} Tff7b72else Tff7b72if Tb4b4b4(Te6edf3outcomeTb4b4b4.Te6edf3wasStableTb4b4b4) Tb4b4b4{
Te6edf3consecutiveFailures Tff7b72= T79c0ff0
Te6edf3ActionTb4b4b4.Te6edf3Continue
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3consecutiveFailuresTff7b72+Tff7b72+
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffUnstable connection (consecutive failures: Tffd700$Te6edf3consecutiveFailuresTa5d6ff)Ta5d6ff" Tb4b4b4}
Te6edf3evaluateFailureTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tff7b72is Te6edf3OutcomeTb4b4b4.Te6edf3Failed Tff7b72-Tff7b72> Tb4b4b4{
Te6edf3consecutiveFailuresTff7b72+Tff7b72+
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffConnection failed (consecutive failures: Tffd700$Te6edf3consecutiveFailuresTa5d6ff)Ta5d6ff" Tb4b4b4}
Te6edf3evaluateFailureTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffevaluateFailureTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3Action Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3consecutiveFailures Tff7b72>Tff7b72= Te6edf3maxFailuresTb4b4b4) Tb4b4b4{
Tff7b72return Te6edf3ActionTb4b4b4.Te6edf3GiveUp
Tb4b4b4}
Tff7b72val Te6edf3backoff Tff7b72= Te6edf3backoffStrategyTb4b4b4(Te6edf3consecutiveFailuresTb4b4b4)
Tff7b72return Tff7b72if Tb4b4b4(Te6edf3consecutiveFailures Tff7b72>Tff7b72= Te6edf3failureThresholdTb4b4b4) Tb4b4b4{
Te6edf3ActionTb4b4b4.Te6edf3SignalTransientTb4b4b4(Te6edf3backoffTb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3ActionTb4b4b4.Te6edf3RetryTb4b4b4(Te6edf3backoffTb4b4b4)
Tb4b4b4}
Tb4b4b4}
T8b949e/**
* Runs the reconnect loop, calling [attempt] for each iteration.
*
* The [attempt] lambda should perform a single connect-and-wait cycle and return the [Outcome] when the connection
* drops or an error occurs.
*
* @param attempt performs a single connection attempt and returns the outcome
* @param onTransientDisconnect called when the policy decides to signal a transient disconnect
* @param onPermanentDisconnect called when the policy gives up permanently
*/
Tff7b72suspend Tff7b72fun Td2a8ffexecuteTb4b4b4(
Te6edf3attemptTb4b4b4: Te6edf3suspend Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Te6edf3OutcomeTb4b4b4,
Te6edf3onTransientDisconnectTb4b4b4: Te6edf3suspend Tb4b4b4(Te6edf3Throwable?Tb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Te6edf3onPermanentDisconnectTb4b4b4: Te6edf3suspend Tb4b4b4(Te6edf3Throwable?Tb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Tb4b4b4) Tb4b4b4{
Tff7b72while Tb4b4b4(Te6edf3coroutineContextTb4b4b4.Te6edf3isActiveTb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(Te6edf3settleDelayTb4b4b4)
Tff7b72val Te6edf3outcome Tff7b72= Te6edf3attemptTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3lastError Tff7b72= Tb4b4b4(Te6edf3outcome Tff7b72as? Te6edf3OutcomeTb4b4b4.Te6edf3FailedTb4b4b4)Tff7b72?.Te6edf3error
Tff7b72when Tb4b4b4(Tff7b72val Te6edf3action Tff7b72= Te6edf3processOutcomeTb4b4b4(Te6edf3outcomeTb4b4b4)Tb4b4b4) Tb4b4b4{
Tff7b72is Te6edf3ActionTb4b4b4.Te6edf3Continue Tff7b72-Tff7b72> Tff7b72continue
Tff7b72is Te6edf3ActionTb4b4b4.Te6edf3Retry Tff7b72-Tff7b72> Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffRetrying in Tffd700${Te6edf3actionTb4b4b4.Te6edf3backoffTffd700}Ta5d6ff (failure #Tffd700$Te6edf3consecutiveFailuresTa5d6ff)Ta5d6ff" Tb4b4b4}
Te6edf3delayTb4b4b4(Te6edf3actionTb4b4b4.Te6edf3backoffTb4b4b4)
Tb4b4b4}
Tff7b72is Te6edf3ActionTb4b4b4.Te6edf3SignalTransient Tff7b72-Tff7b72> Tb4b4b4{
Te6edf3onTransientDisconnectTb4b4b4(Te6edf3lastErrorTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffRetrying in Tffd700${Te6edf3actionTb4b4b4.Te6edf3backoffTffd700}Ta5d6ff (failure #Tffd700$Te6edf3consecutiveFailuresTa5d6ff)Ta5d6ff" Tb4b4b4}
Te6edf3delayTb4b4b4(Te6edf3actionTb4b4b4.Te6edf3backoffTb4b4b4)
Tb4b4b4}
Tff7b72is Te6edf3ActionTb4b4b4.Te6edf3GiveUp Tff7b72-Tff7b72> Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3e Tb4b4b4{ Ta5d6ff"Ta5d6ffGiving up after Tffd700$Te6edf3consecutiveFailuresTa5d6ff consecutive failuresTa5d6ff" Tb4b4b4}
Te6edf3onPermanentDisconnectTb4b4b4(Te6edf3lastErrorTb4b4b4)
Tff7b72return
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72companion Tff7b72object Tb4b4b4{
Tff7b72const Tff7b72val Te6edf3DEFAULT_MAX_FAILURES Tff7b72= T79c0ff1T79c0ff0
Tff7b72const Tff7b72val Te6edf3DEFAULT_FAILURE_THRESHOLD Tff7b72= T79c0ff3
T8b949e/**
* Delay applied before every connection attempt (including the first) so the BLE stack and the firmware-side
* GATT session have time to settle.
*
* Empirically validated against the meshtastic-client KMP SDK probes (Apr 2026): with a 1.5 s pause between
* disconnect→reconnect cycles, 3/5–4/5 attempts failed mid-handshake (Stage1Draining timeouts) because the
* firmware had not yet released its GATT session from the previous cycle. With ≥ 5 s pause, success rate rose
* to 5/5 against a strong (-53 dBm) link. 3 s is a conservative compromise on Android, whose BLE stack is more
* mature than btleplug+CoreBluetooth, but the firmware-side cleanup constraint is the same.
*/
Tff7b72val Te6edf3DEFAULT_SETTLE_DELAY Tff7b72= T79c0ff3.Te6edf3seconds
Tff7b72val Te6edf3DEFAULT_MIN_STABLE_CONNECTION Tff7b72= T79c0ff5.Te6edf3seconds
Tff7b72internal Tff7b72val Te6edf3RECONNECT_BASE_DELAY Tff7b72= T79c0ff5.Te6edf3seconds
Tff7b72internal Tff7b72val Te6edf3RECONNECT_MAX_DELAY Tff7b72= T79c0ff6T79c0ff0.Te6edf3seconds
Tff7b72internal Tff7b72const Tff7b72val Te6edf3BACKOFF_MAX_EXPONENT Tff7b72= T79c0ff4
Tb4b4b4}
Tb4b4b4}
T8b949e/**
* Returns the reconnect backoff delay for a given consecutive failure count.
*
* Backoff schedule: 1 failure → 5 s, 2 failures → 10 s, 3 failures → 20 s, 4 failures → 40 s, 5+ failures → 60 s
* (capped).
*/
Tff7b72internal Tff7b72fun Td2a8ffcomputeReconnectBackoffTb4b4b4(Te6edf3consecutiveFailuresTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Te6edf3Duration Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3consecutiveFailures Tff7b72<Tff7b72= T79c0ff0Tb4b4b4) Tff7b72return Te6edf3BleReconnectPolicyTb4b4b4.Te6edf3RECONNECT_BASE_DELAY
Tff7b72val Te6edf3multiplier Tff7b72= T79c0ff1 Te6edf3shl Tb4b4b4(Te6edf3consecutiveFailures Tff7b72- T79c0ff1Tb4b4b4)Tb4b4b4.Te6edf3coerceAtMostTb4b4b4(Te6edf3BleReconnectPolicyTb4b4b4.Te6edf3BACKOFF_MAX_EXPONENTTb4b4b4)
Tff7b72return Te6edf3minOfTb4b4b4(Te6edf3BleReconnectPolicyTb4b4b4.Te6edf3RECONNECT_BASE_DELAY Tff7b72* Te6edf3multiplierTb4b4b4, Te6edf3BleReconnectPolicyTb4b4b4.Te6edf3RECONNECT_MAX_DELAYTb4b4b4)
Tb4b4b4}
Served by rngit 1.5.0 - Generated in 0.06s